﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Linq;
using System.Collections.Generic;
using System.Management.Instrumentation;
using System.Threading;
using Microsoft.Ccr.Core;

namespace XcoAppSpaces.Dialogs
{
    public class Dialog : Port<DialogMessage>
    {
        private DispatcherQueue taskQueue;
        internal protected SynchronizationContext syncContext;
        internal protected IList<IDialogMessageHandler> handlers;
        private IPort replyPort;

        private Port<DateTime> replyTimeoutPort;
        private Timer replyTimeoutTimer;
        private Receiver replyTimeoutReceiver;


        public Dialog()
        {
            this.syncContext = SynchronizationContext.Current;
            this.handlers = new List<IDialogMessageHandler>();
        }


        public void Start() { Start(new DispatcherQueue()); }
        public void Start(DispatcherQueue dpq)
        {
            if (DialogHasNotBeenStarted())
            {
                InitializeTaskQueue(dpq);

                InitializeReplyTimeout();
                ResetReplyTimeout();

                Define();

                WaitForNextMessage();
            }
        }


            private bool DialogHasNotBeenStarted()
            {
                return this.taskQueue == null;
            }

            private void InitializeTaskQueue(DispatcherQueue dpq)
            {
                if (dpq == null) dpq = new DispatcherQueue();
                this.taskQueue = dpq;
            }

            private void InitializeReplyTimeout()
            {
                this.replyTimeoutPort = new Port<DateTime>();
                this.replyTimeoutTimer = new Timer(x => this.replyTimeoutPort.Post(DateTime.Now), null, Timeout.Infinite,
                                                   Timeout.Infinite);
            }


        private void ResetReplyTimeout()
            {
                this.replyTimeoutTimer.Change(Timeout.Infinite, Timeout.Infinite);
                this.replyTimeoutReceiver = this.replyTimeoutPort.Receive(dt => { throw new InvalidOperationException("Dialog received an unexpected timeout message!"); });
            }

            private void SetReplyTimeout(int timeout, Action<DateTime> timeoutAction)
            {
                this.replyTimeoutTimer.Change(timeout, Timeout.Infinite);
                this.replyTimeoutReceiver = this.replyTimeoutPort.Receive(
                    timeoutTimestamp =>
                        {
                            ResetReplyTimeout();
                            try
                            {
                                timeoutAction(timeoutTimestamp);
                            }
                            finally
                            {
                                WaitForNextMessage();
                            }
                        }
                    );
            }


        protected virtual void Define()
        {}


        //TODO: how should iterator message handlers be called? are they allowed to run in the caller´s sync ctx?
        private void WaitForNextMessage()
        {
            Arbiter.Activate(
                this.taskQueue,
                Arbiter.Choice(
                    this.replyTimeoutReceiver,
                    this.Receive(ProcessMessagesSequentially)
                    )
                );
        }


            private void ProcessMessagesSequentially(DialogMessage message)
            {
                try
                {
                    ProcessMessage(message);
                }
                finally
                {
                    WaitForNextMessage();
                }
            }


                private void ProcessMessage(DialogMessage message)
                {
                    this.replyPort = message.ReplyPort;

                    IDialogMessageHandler dialogMessageHandler = FindMessageHandler(message);
                    ExecuteMessageHandler(message, dialogMessageHandler);
                }


                    private IDialogMessageHandler FindMessageHandler(DialogMessage message)
                    {
                        return this.handlers.FirstOrDefault(dmh => dmh.Match(message));
                    }
                    
                    private void ExecuteMessageHandler(DialogMessage message, IDialogMessageHandler dialogMessageHandler)
                    {
                        if (dialogMessageHandler != null)
                            dialogMessageHandler.Execute(message.Payload);
                        else
                            throw new InstanceNotFoundException(string.Format("No matching dialog message handler found for message type {0}!", message.Payload.GetType()));
                    }


        public void Handler<TMessage>(Handler<TMessage> handler) { Handler(handler, false); }
        public void Handler<TMessage>(Handler<TMessage> handler, bool handleInSyncContext)
        {
            this.handlers.Add(new DialogMessageHandler<TMessage>(handler, handleInSyncContext ? this.syncContext : null));
        }


        public void Reply(object message)
        {
            this.replyPort.PostUnknownType(new DialogMessage(message, this));
        }

        public void Reply(object message, int timeout, Action<DateTime> timeoutAction)
        {
            SetReplyTimeout(timeout, timeoutAction);
            Reply(message);
        }
    }
}
